﻿<!doctype html />
<html>
<head>
	<title>Timeline</title>
	<!-- Timeline definition -->
	<script type="text/javascript">
		function drawTick(x, text, event, callback, textXoffset, textYoffset) {
			context.beginPath();
			context.moveTo(x, canvas.height / 2 - HALFTICKLENGTH);
			context.lineTo(x, canvas.height / 2 + HALFTICKLENGTH);
			drawText(text, x + textXoffset, canvas.height / 2 + HALFTICKLENGTH + 2 + textYoffset, event, callback);

			context.stroke();
			context.closePath();
		}

		function Record(shape, x1, y1, x2, y2, event, callback) {
			this.shape = shape;
			this.x1 = x1;
			this.y1 = y1;
			this.x2 = x2;
			this.y2 = y2;
			this.event = event;
			this.callback = callback;
		}

		function RectRecord(shape, x, y, width, height, event, callback) {
			this.shape = shape;
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.event = event;
			this.callback = callback;
		}

		/*
		The RecordKeeper maintains a list of important text drawn to the screen, and registers an onclick handler for each rectangle.
		*/
		function RecordKeeper() {
			RecordKeeper.pointOnRectangle = function (pointX, pointY, rectX, rectY, width, height) {
				if ((width > 0 ? pointX >= rectX && pointX <= rectX + width : pointX <= rectX && pointX >= rectX + width) &&
(height > 0 ? pointY >= rectY && pointY <= rectY + height : pointY <= rectY && pointY >= rectY + height)) {
					return true;
				}
				return false;
			}

			RecordKeeper.pointOnLine = function (pointX, pointY, lineX1, lineY1, lineX2, lineY2) {
				if (pointX >= lineX1 && pointX <= lineX2 && pointY >= lineY1 && pointY <= lineY2) {
					return true;
				}

				return false;
			}

			RecordKeeper.records = [];
			RecordKeeper.push = function (item) {
				RecordKeeper.records.push(item);
			};

			RecordKeeper.clear = function () {
				RecordKeeper.records.length = 0;
			};

			document.onclick = function (e) {
				//console.log(e.pageX + ', ' + e.pageY);
				for (var i = RecordKeeper.records.length - 1; i >= 0; i--) {
					var record = RecordKeeper.get(i);
					//console.log('record: ' + record.event.name + '(' + record.x + ',' + record.y + '), (' + record.width + ', ' + record.height + ')');
					if (record.shape == 'rectangle')
						if (RecordKeeper.pointOnRectangle(e.pageX, e.pageY, record.x, record.y, record.width, record.height)) {
							record.callback(record);
							return;
						}
				}
			};

			RecordKeeper.length = function () {
				return RecordKeeper.records.length;
			};

			RecordKeeper.get = function (i) {
				return RecordKeeper.records[i];
			};
		}

		function Event(start, end, name, text) {
			this.start = start;
			this.end = end;
			this.name = name;
			this.text = text;
		}

		/*
		Background class. This class is responsible for supplying events to calendar before
		they are drawn. To use, simply call class definition, and then its member methods.
		This class functions like a Priority Queue, where priority is indicated by the 
		start time.
            
		methods:
		EventCollection(),
		EventCollection.get(i), retrieve the ith event. No bounds checking done inside.
		EventCollection.size(), returns the number of items in the EventCollection
		EventCollection.remove(i), remove the ith member. No bounds checking done inside.
		*/
		function EventCollection() {
			EventCollection.events = [];
			EventCollection.EVENT_INDEX = 0;
			EventCollection.LEVEL_INDEX = 1;
			/*
			EventCollection.push() inserts a value into the EventCollection at level 0.
			*/
			EventCollection.push = function push(event) {

				// find position for event
				var pos = 0;
				for (var i = 0; i < EventCollection.size() && EventCollection.get(i).start < EventCollection.get(i).start; i++)
					pos = i;

				// insert event
				eventData = new Array();
				eventData.push(event);
				eventData.push(0);
				EventCollection.events.splice(pos, 0, eventData);
			}

			EventCollection.get = function getEvent(i) {
				return EventCollection.events[i][EventCollection.EVENT_INDEX];
			}

			EventCollection.size = function () {
				return EventCollection.events.length;
			}

			EventCollection.remove = function (i) {
				EventCollection.events.splice(i, 1);
			}

			/*
			This function returns an integer, to be used for determining how far away from the number line
			an event should be drawn.
			The goal here is to have the closest event at the bottom, stacking events on top,
			if they overlap and simply placing them next to each other when they're after
			each other.
			Very little bounds checking is done here. Don't rely on it.
			min value: 1
			*/
			EventCollection.getLevel = function (i) {
				return EventCollection.events[i][EventCollection.LEVEL_INDEX];
			}

			EventCollection._eventShown = function (i) {
				var starts_after_now = EventCollection.get(i).start >= now.getTime();
				var starts_before_end = EventCollection.get(i).start <= later.getTime();
				return starts_after_now || starts_before_end;
			}

			EventCollection._doTheyOverlap = function (event2, event1) {
				if (event1.start <= event2.end)
					return true;

				if (event2.start <= event1.end)
					return 2;
			}

			/*
			Requires later to be defined. (to be the end of the timeline)
			*/
			EventCollection.calculateLevels = function () {
				/*
				Implementing getLevel(): Ok, so here is the world.
                    
				Assumption: Calculating an entry's level requires O(n) lookups.
				Using a table to calculate all tables at once reduces complexity from
				O(n^2) to O(n).

				Values for i's not show are undefined at this point.
				*/

				var a_level;
				// alert("size: " + EventCollection.size());
				for (var i = 0; i < EventCollection.size(); i++) {
					if (i == 0)
						a_level = 1;
					// alert('i: ' + i);
					if (i > 0 && EventCollection._eventShown(i)) {
						// if the current event's start is before the previous (EventCollevtion) 
						// event's end, increment the level.
						// if the current event's start is equal to the previous event's start,
						// increment level
						// alert('_doTheyOverlap?\n'+'1: ' + EventCollection.get(i).start + '-' + EventCollection.get(i).end + '\n2: ' + EventCollection.get(i - 1).start + '-' + EventCollection.get(i - 1).end + '\n' + (EventCollection._doTheyOverlap(EventCollection.get(i), EventCollection.get(i - 1))?'true':'false'));
						if (EventCollection._doTheyOverlap(EventCollection.get(i), EventCollection.get(i - 1))) {
							a_level = EventCollection.getLevel(i - 1) + 1;
						}
					}
					// alert('level: ' + a_level);
					EventCollection.events[i][EventCollection.LEVEL_INDEX] = a_level;
				}
			}

			EventCollection.clear = function () {
				EventCollection.events.length = 0;
			}
		}

		function eventclicked(record) {
			var event = record.event;
			alert(event.name);
		}

		function drawText(text, x, y, event, callback) {
			var width = context.measureText(text).width;
			//context.beginPath();
			context.fillText(text, x, y);
			//context.fillRect(x, y, width, 12);
			//context.stroke();
			//context.closePath();
			var record = new RectRecord('rectangle', x, y, width, 12, event, eventclicked);
			RecordKeeper.push(record);
		}

		// no error checking!
		function displayEvent(i) {

			var level = EventCollection.getLevel(i);
			var LEVEL_DISTANCE = 15;
			var event = EventCollection.get(i);

			// draw the event line
			var beginX = BORDER + (event.start - now.getTime()) * scale;
			if (beginX < BORDER)
				beginX = 0;
			var beginY = canvas.height / 2 - ((level) * LEVEL_DISTANCE);

			var endX = BORDER + (event.end - now.getTime()) * scale;
			if (endX > canvas.width)
				endX = canvas.width;
			var endY = beginY;

			context.beginPath();
			context.moveTo(beginX, beginY);
			context.lineTo(endX, endY);

			context.stroke();
			context.closePath();


			// draw the text for the event
			// pushing an approximation of the rectangle onto the RecordKeeper
			var beginning = beginX;
			var middleish = beginX + (endX - beginX) / 2;
			var end = endX
			var rectX = beginning > BORDER ? beginning : (middleish > BORDER ? middleish : end);
			var rectY = beginY - 3;

			drawText(event.name, rectX, rectY, event, eventclicked);
		}

		function setup() {

			canvas = document.getElementById("canvas");
			context = canvas.getContext('2d');
			context.font = '12px mono sans serif';
			var margin = 50;
			WIDTH = canvas.width;
			HEIGHT = canvas.height;
			new RecordKeeper();
			console.log('RecordKeeper instantiated');

			SCALE_DEFAULT = 50;
			SLIDER_MIN = 0.000001;
			SLIDER_MAX = 1;
			HALFTICKLENGTH = 10;
			$("#range").onchange = function () {
				var sc/*ale*/ = logslider(document.getElementById("range").value);
				scale = sc;
			}
			console.log('$(#range).onchange set');
			scale = logslider($("#range").val());
			console.log('initial scale set');

			new EventCollection();
			console.log('EventCollection initiated');

			BORDER = 10;
			now = new Date();
			var currentTime = now.getTime();

			var range = document.getElementById('range');
			range.onchange = function () {
				scale = logslider(range.value);
			}
		}

		function logslider(value) {
			var min = 0;
			var max = 100;

			var minv = Math.log(SLIDER_MIN);
			var maxv = Math.log(SLIDER_MAX);

			var sc/*ale*/ = (maxv - minv) / (max - min);

			return Math.exp(minv + sc * (value - min));
		}

		function timeclicked(record) {
			var event = record.event;
			alert(event.name);
		}

		function labelHours(now, later) {
			// determine the next full hour
			var lastHour = (now.getTime() / (1000 * 60 * 60)) * (1000 * 60 * 60);
			var nextHour = lastHour + (1000 * 60 * 60);
			for (var i = nextHour; i < later.getTime(); i += (1000 * 60 * 60)) {
				var date = new Date(i);
				var event = new Event(0, 0, date.getHours(), '');
				drawTick(i * scale, date.getHours(), event, timeclicked, 0, 0);
			}
		}

		function drawTicks() {
			/* 
			Hourly tick marks

			Currently working on this one.
			Here are some thoughts in the meantime.
			Given two javascript Date objects, 'now' and 'later', how to draw tickmarks on every hour along the timeline.

			1. determine the lasthour = now.getTime() / (1000 * 60 * 60). This will give me the total number of hours since the epoch
			2. determine the next hour by nexthour = lasthour + (1000 * 60 * 60). This gives me the next full hour since the epoch
			3. from now on, for i=nexthour; i<later.getTime; i += (1000 * 60 * 60) drawTick(...

			4. Now for the tricky part. Scaling the timevalue into a pixel value.

			*/

			// draw hourly tickmarks

			function timeToLine(time) {
				var minIn = now.getTime();
				var maxIn = later.getTime();

				var minOut = Math.log(BORDER);
				var maxOut = Math.log(canvas.width - BORDER * 2);

				var sc/*ale*/ = (maxOut - minOut) / (maxIn - minIn);

				return Math.exp(minOut + sc * (time - minIn));
			}

			var msInHour = 1000 * 60 * 60;

			var lastHour = now.getTime() / msInHour * msInHour; // yay integer division
			var nextHour = lastHour + msInHour;
			for (var i = nextHour; i < later.getTime(); i += msInHour) {
				var hour = new Date(i);
				var event = new Event(0, 0, hour.getHours(), '');
				drawTick(timeToLine(i), hour.getHours(), event, timeclicked, 0, 0);
			}
		}

		function draw() {
			context.clearRect(0, 0, WIDTH, HEIGHT);

			RecordKeeper.clear();

			// draw timeline
			now = new Date();
			//context.strokeStyle('#000');
			context.beginPath();

			var x1 = 0, y1 = canvas.height / 2;
			var x2 = canvas.width, y2 = canvas.height / 2;
			context.moveTo(x1, y1);
			context.lineTo(x2, y2);

			context.stroke();
			context.closePath();

			// draw now tickmark
			var event = new Event(0, 0, 'now', '');
			drawTick(BORDER, now.toTimeString() + ', ' + now.toDateString(), event, timeclicked, 0, 0);

			// draw later tickmark
			context.beginPath();
			later = new Date(now.getTime() + (canvas.width - (BORDER * 2)) / scale);
			var text = later.toTimeString() + ', ' + later.toDateString();

			var event = new Event(0, 0, later.toTimeString(), '');
			drawTick(canvas.width - BORDER, text, event, timeclicked, -1 * context.measureText(text).width, 0);

			// display span next to the label
			document.getElementById("label").innerText = (later.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) + " days";
			context.stroke();
			context.closePath();

			// drawTicks();


			// draw events
			EventCollection.calculateLevels();
			for (var i = 0; i < EventCollection.size(); i++) {
				if (EventCollection.get(i).end < now.getTime()) {
					EventCollection.remove(i);
					return;
				} else {
					displayEvent(i);
				}
			}
		}
	</script>
	<!-- end of Timeline Code (more or less) -->
	<!-- gdata code -->
	<script type="text/javascript" src="http://www.google.com/jsapi"></script>
	<script type="text/javascript">

		google.load("gdata", "2.x", { packages: ['calendar'] });
		google.load('jquery', '1.4.4');

		var SCOPE = 'https://www.google.com/calendar/feeds';

		function login() {
			var token = google.accounts.user.login(SCOPE);
		}

		function logout() {
			google.accounts.user.logout();
			$("#log").text("Logging out");
			var status = google.accounts.user.getStatus();
			if (status == google.accounts.AuthSubStatus.LOGGED_OUT)
				$("#log").text("Logged out");
			// remove calendars
		}

		function setupLogin() {
			$("#logout").attr('disabled', 'disabled');
			$("#login").removeAttr('disabled');
		}

		function setupLogout() {
			$("#login").attr('disabled', 'disabled');
			$("#logout").removeAttr('disabled');
		}

		function onError(result) {
			alert("What we've got here -- is a failure to communicate")
		}

		function onLoad(result) {
			var entries = result.feed.getEntries();
			for (var i = 0; i < entries.length; i++) {
				var entry = entries[i];
				var something = entry.getTimes();
				var start = Date.parse(something[0].startTime);
				var end = Date.parse(something[0].endTime);

				var event = new Event(start, end, entry.getTitle().getText(), entry.getSelfLink());
				EventCollection.push(event);
				//console.log(JSON.stringify(event));
			}
		}

		google.setOnLoadCallback(function () {
			var status = google.accounts.user.getStatus(SCOPE);

			if (status == google.accounts.AuthSubStatus.LOGGING_IN) {
				$("#log").text("Loggin in");
				return;
			} else if (status == google.accounts.AuthSubStatus.LOGGED_OUT) {
				setupLogin();
				$("#log").text("Logged out");
				return;
			} else {
				// user is logged in. 
				// Yay

				$("#log").text("Logged in");
				var margin = 50; // A number to be subtracted from window.width to prevent the bottom scroll bar
				$("#canvas_wrapper").append($("<canvas id=\"canvas\" height=\"500\" width=\"" + ($(window).width() - margin) + "\" />"));

				setup();
				setInterval('draw()', 30);

				gcal = new google.gdata.calendar.CalendarService('Timeline');
				var feedUrl = 'https://www.google.com/calendar/feeds/default/private/full';
				var query = new google.gdata.calendar.CalendarEventQuery(feedUrl);
				query.setOrderBy('starttime');
				query.setSortOrder('ascending');
				query.setFutureEvents(true);
				query.setSingleEvents(true);
				query.setMaxResults(5);
				gcal.getEventsFeed(query, onLoad, onError);
			}
		});

	</script>
	<!-- testing code -->
	<script type="text/javascript">

		RUN_TESTS = false;

		function run_test(foo) {
			if (RUN_TESTS) {
				var result = foo();
				console.log((result ? 'OK' : 'FAIL') + ' ' + foo.name);
			}
		}

		function testEventCollection() {
			var result = true;
			now = new Date();
			later = new Date(now.getTime() + (canvas.width - (BORDER * 2)) / scale);
			new EventCollection();

			var time = new Date();
			var event1 = new Event(time.getTime() + 5000, time.getTime() + 10000, 'event1', '');
			var event2 = new Event(time.getTime() + 7000, time.getTime() + 12000, 'event2', '');

			EventCollection.push(event1);
			EventCollection.push(event1);

			console.log('test_size: ' + EventCollection.size());

			EventCollection.calculateLevels();
			var level1 = EventCollection.getLevel(0);
			var level2 = EventCollection.getLevel(1);

			console.log('test_level1: ' + level1);
			console.log('test_level2: ' + level2);

			if (level1 == 1 && level2)
				return true;
			else
				return false;
		}

		if (RUN_TESTS) {
			run_test(testEventCollection);
		}

	</script>
	<link rel="Stylesheet" href="stylesheet.css" />
</head>
<body>
	<div id="content">
		<nav>
			<span class="border left"> <a id="login" href="javascript:login()">Log in</a> | <span id="log"></span> | <a id="logout" href="javascript:logout()">Log out</a>
			</span>
			<span class="right"><a href="getting-started.html">Manual</a></span>
		</nav><br />
		<h2 id="title">
			Timeline</h2>
		<h3 id="subtitle">
			A Google Calendar Interface</h3>
		<hr />
		<div id="canvas_wrapper">
		</div>
		<label for="range">
			Scale:
		</label>
		<input type="range" id="range" min="0" max="100" step="1" value="50" />
		<div id="label">
		</div>
		<!-- there so google and gdata can perform their jobs -->
		<img src="onepixel.png" alt='' />
	</div>
</body>
</html>
